14.3 Skalierung von Daten#
Lernziele
Sie wissen, dass Daten skaliert werden sollten. Ausnahme: Decision Tree oder Randon Forest.
Sie können Daten normieren.
Sie können Daten standardisieren.
Beispiel: Weinqualität#
Der folgende Datensatz stammt vom UCI Machine Learning Repository
https://archive.ics.uci.edu/dataset/186/wine+quality
und wurde ursprünglich in dieser Publikation betrachtet: http://www3.dsi.uminho.pt/pcortez/wine5.pdf
Input sind physikalische und chemische Messungen, Output ist die Qualität des Weines von 0 (sehr schlecht) bis 10 (exzellent). Wie üblich laden wir den Datensatz:
import pandas as pd
data = pd.read_csv('data/winequality_red_DE.csv', skiprows=2)
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1599 entries, 0 to 1598
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 fester Säuregehalt 1599 non-null float64
1 flüchtiger Säuregehalt 1599 non-null float64
2 Zitronensäure 1599 non-null float64
3 Restzucker 1599 non-null float64
4 Chloride 1599 non-null float64
5 freies Schwefeldioxid 1599 non-null float64
6 Gesamtschwefeldioxid 1599 non-null float64
7 Dichte 1599 non-null float64
8 pH-Wert 1599 non-null float64
9 Sulfate 1599 non-null float64
10 Alkohol 1599 non-null float64
11 Qualität 1599 non-null int64
dtypes: float64(11), int64(1)
memory usage: 150.0 KB
Der Datensatz zur Weinqualität enthält 1599 Einträge mit 12 Eigenschaften. Die ersten 11 Eigenschaften werden durch Floats repräsentiert, nur die letzte Eigenschaft ‘Qualität’ wird durch Integers repräsentiert. Alle Einträge sind gültig.
data.head()
| fester Säuregehalt | flüchtiger Säuregehalt | Zitronensäure | Restzucker | Chloride | freies Schwefeldioxid | Gesamtschwefeldioxid | Dichte | pH-Wert | Sulfate | Alkohol | Qualität | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7.4 | 0.70 | 0.00 | 1.9 | 0.076 | 11.0 | 34.0 | 0.9978 | 3.51 | 0.56 | 9.4 | 5 |
| 1 | 7.8 | 0.88 | 0.00 | 2.6 | 0.098 | 25.0 | 67.0 | 0.9968 | 3.20 | 0.68 | 9.8 | 5 |
| 2 | 7.8 | 0.76 | 0.04 | 2.3 | 0.092 | 15.0 | 54.0 | 0.9970 | 3.26 | 0.65 | 9.8 | 5 |
| 3 | 11.2 | 0.28 | 0.56 | 1.9 | 0.075 | 17.0 | 60.0 | 0.9980 | 3.16 | 0.58 | 9.8 | 6 |
| 4 | 7.4 | 0.70 | 0.00 | 1.9 | 0.076 | 11.0 | 34.0 | 0.9978 | 3.51 | 0.56 | 9.4 | 5 |
Ein erster Blick auf die Daten zeigt bereits, dass die Eigenschaftswerte in unterschiedlichen Bereichen liegen. Der feste Säuregehalt beispielsweise scheint zwischen 7 und 11 zu liegen, wohingegen Cloride scheinbar eher im Bereich 0.076 bis 0.098 liegen. Das zeigt auch die Übersicht der statistischen Kennzahlen:
data.describe()
| fester Säuregehalt | flüchtiger Säuregehalt | Zitronensäure | Restzucker | Chloride | freies Schwefeldioxid | Gesamtschwefeldioxid | Dichte | pH-Wert | Sulfate | Alkohol | Qualität | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 |
| mean | 8.319637 | 0.527821 | 0.270976 | 2.538806 | 0.087467 | 15.874922 | 46.467792 | 0.996747 | 3.311113 | 0.658149 | 10.422983 | 5.636023 |
| std | 1.741096 | 0.179060 | 0.194801 | 1.409928 | 0.047065 | 10.460157 | 32.895324 | 0.001887 | 0.154386 | 0.169507 | 1.065668 | 0.807569 |
| min | 4.600000 | 0.120000 | 0.000000 | 0.900000 | 0.012000 | 1.000000 | 6.000000 | 0.990070 | 2.740000 | 0.330000 | 8.400000 | 3.000000 |
| 25% | 7.100000 | 0.390000 | 0.090000 | 1.900000 | 0.070000 | 7.000000 | 22.000000 | 0.995600 | 3.210000 | 0.550000 | 9.500000 | 5.000000 |
| 50% | 7.900000 | 0.520000 | 0.260000 | 2.200000 | 0.079000 | 14.000000 | 38.000000 | 0.996750 | 3.310000 | 0.620000 | 10.200000 | 6.000000 |
| 75% | 9.200000 | 0.640000 | 0.420000 | 2.600000 | 0.090000 | 21.000000 | 62.000000 | 0.997835 | 3.400000 | 0.730000 | 11.100000 | 6.000000 |
| max | 15.900000 | 1.580000 | 1.000000 | 15.500000 | 0.611000 | 72.000000 | 289.000000 | 1.003690 | 4.010000 | 2.000000 | 14.900000 | 8.000000 |
Schwankt beispielsweise die Dichte zwischen 0.990070 und 1.003690, so liegen die Gesamtschwefeldioxid-Werte zwischen 6 und 289 in einer völlig anderen Größenordnung.
Damit ist auch der Boxplot nicht mehr lesbar:
import plotly.express as px
fig = px.box(data,
title='Eigenschaften Rotwein',
labels={'variable': 'Eigenschaft', 'value': 'Werte'})
fig.show()
Das hat auch Auswirkungen auf das Training der ML-Modelle.
Zunächst interpretieren wir die Prognose der Weinqualität als Klassifikationsproblem und setzen eine 1 für guten Wein (Qualität 6 und mehr) und eine 0 für schlechten Wein (Qualität bis einschließlich 5).
data_classification = data.copy()
data_classification.replace(3, 0, inplace=True) # schlechter Wein
data_classification.replace(4, 0, inplace=True) # schlechter Wein
data_classification.replace(5, 0, inplace=True) # schlechter Wein
data_classification.replace(6, 1, inplace=True) # guter Wein
data_classification.replace(7, 1, inplace=True) # guter Wein
data_classification.replace(8, 1, inplace=True) # guter Wein
Als nächstes trainieren wir ein neuronales Netz.
# Adaption der Daten
from sklearn.model_selection import train_test_split
X = data_classification.loc[:, 'fester Säuregehalt' : 'Alkohol']
y = data_classification['Qualität']
X_train, X_test, y_train, y_test = train_test_split(X,y, random_state=42)
# Training neuronales Netz
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split
# Auswahl des Models
# solver = 'lbfgs' für kleine Datenmengen, solver = 'adam' für große Datenmengen, eher ab 10000
# hidden_layer: Anzahl der Neuronen pro verdeckte Schicht und Anzahl der verdeckten Schichten
model = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[5, 5], random_state=42)
# Training
model.fit(X_train, y_train)
# Validierung
score_train = model.score(X_train, y_train)
score_test = model.score(X_test, y_test)
print(f'Score für Trainingsdaten: {score_train:.2f}')
print(f'Score für Testdaten: {score_test:.2f}')
Score für Trainingsdaten: 0.75
Score für Testdaten: 0.73
/opt/homebrew/Caskroom/miniconda/base/envs/python312/lib/python3.12/site-packages/sklearn/neural_network/_multilayer_perceptron.py:546: ConvergenceWarning:
lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.
Increase the number of iterations (max_iter) or scale the data as shown in:
https://scikit-learn.org/stable/modules/preprocessing.html
Auf der einen Seite erhalten wir ein Resultat, aber auf der anderen Seite gibt es auch eine Fehlmeldung.
lbfgs failed to converge (status=1): STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.
Tatsächlich ist der Algorithmus nicht zu einem “richtigen” Ergebnis gekommen, er ist nicht konvergiert. Scikit-Learn schlägt auch vor, wie wir den Algorithmus unterstützen können. Wir könnten die Anzahl der Iterationen erhöhen in der Hoffnung, dass dann der Algorithmus ein konvergentes Ergebnis erreicht, oder die Daten skalieren.
Wir betrachten daher beide Möglichkeiten. Zuerst setzen wir die Anzahl der Iterationen hoch. Die Dokumentation von Scikit-Learn gibt an, dass der Parameter ‘max_iter’ heißt und normalerweise auf 200 gesetzt ist. Wir setzen ihn auf 10000:
model = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[5, 5], max_iter=10000, random_state=42)
# Training
model.fit(X_train, y_train)
# Validierung
score_train = model.score(X_train, y_train)
score_test = model.score(X_test, y_test)
print(f'Score für Trainingsdaten: {score_train:.2f}')
print(f'Score für Testdaten: {score_test:.2f}')
Score für Trainingsdaten: 0.76
Score für Testdaten: 0.73
Hat funktioniert :-) Als nächstes betrachten wir noch das Skalieren der Daten. Außer bei Decision Trees / Random Forests sollten Daten immer skaliert werden.
Skalieren von Daten#
Sind die Bereich der Daten von ihren Zahlenwerten sehr verschieden, sollten alle numerischen Werte in dieselbe Größenordnung gebracht werden. Dieser Vorgang heißt Skalieren der Daten. Gebräulich sind dabei zwei verschiedene Methoden:
Normierung und
Standardisierung.
Normierung#
Bei der Normierung wird festgelegt, dass alle Zahlenwerte in einem festen Intervall liegen. Besonders häufig wrid das Intervall \([0,1]\) genommen. Die Dichte, die zwischen 0.990070 und 1.003690 liegt, würde so transformiert werden, dass das Minimum 0.990070 der 0 entspricht und das Maximum 1.003690 der 1. Genauso würde mit den anderen Eigenschaften verfahren werden. Wir nutzen zur praktischen Umsetzung Scikit-Learn.
from sklearn.preprocessing import MinMaxScaler
# Auswahl Normierung
normierung = MinMaxScaler()
# Analyse: jede Spalte wird auf ihr Minimum und ihre Maximum hin untersucht
# es werden immer die Trainingsdaten verwendet
normierung.fit(X_train)
# Transformation der Trainungs- und Testdaten
X_train_normiert = normierung.transform(X_train)
X_test_normiert = normierung.transform(X_test)
Wir schauen in ‘X_train_normiert’ hinein:
print(X_train_normiert)
[[0.73584906 0.25342466 0.49 ... 0.79551122 0.03680982 0.12307692]
[0.55345912 0.32876712 0.29 ... 0.83790524 0.07361963 0.10769231]
[0.44654088 0.32191781 0. ... 0.85286783 0.11042945 0.47692308]
...
[0.45283019 0.34246575 0.06 ... 0.87531172 0.10429448 0.16923077]
[0.49685535 0.05479452 0.35 ... 0.82793017 0.26380368 0.53846154]
[0.36477987 0.11643836 0.26 ... 0.84538653 0.10429448 0.78461538]]
Es werden zwar nicht alle Werte angezeigt, aber die Normierung der Daten scheint funktioniert zu haben.
Jetzt trainieren wir das neuronale Netz erneut.
# Auswahl des Models
# solver = 'lbfgs' für kleine Datenmengen, solver = 'adam' für große Datenmengen, eher ab 10000
# hidden_layer: Anzahl der Neuronen pro verdeckte Schicht und Anzahl der verdeckten Schichten
model = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[5, 5], max_iter=10000, random_state=42)
# Training
model.fit(X_train_normiert, y_train)
# Validierung
score_train = model.score(X_train_normiert, y_train)
score_test = model.score(X_test_normiert, y_test)
print(f'Score für Trainingsdaten: {score_train:.2f}')
print(f'Score für Testdaten: {score_test:.2f}')
Score für Trainingsdaten: 0.79
Score für Testdaten: 0.73
Der Score der Traingsdaten ist leicht besser geworden.
Standardisierung#
Oft sind Daten normalverteilt. Die Standardisierung berücksichtigt das und transformiert nicht auf ein festes Intervall, sondern verschiebt den Mittelwert auf 0 und die Varianz auf 1. Die normalverteilten Daten werden also standardnormalverteilt. Auch das lassen wir Scikit-Learn erledigen:
from sklearn.preprocessing import StandardScaler
# Auswahl Normierung
skalierung = StandardScaler()
# Analyse: jede Spalte wird auf ihr Minimum und ihre Maximum hin untersucht
# es werden immer die Trainingsdaten verwendet
skalierung.fit(X_train)
# Transformation der Trainungs- und Testdaten
X_train_skaliert = skalierung.transform(X_train)
X_test_skaliert = skalierung.transform(X_test)
print(X_train_skaliert)
[[ 1.5132922 -0.23260309 1.11458849 ... -0.45444422 -1.3131938
-1.15257747]
[ 0.36071333 0.37802632 0.09088663 ... 0.23998089 -0.97064635
-1.24703683]
[-0.31493635 0.32251456 -1.39348108 ... 0.4850721 -0.62809889
1.01998773]
...
[-0.27519225 0.48904985 -1.08637052 ... 0.85270892 -0.68519014
-0.8691994 ]
[ 0.00301644 -1.84244427 0.39799719 ... 0.07658675 0.79918216
1.39782516]
[-0.83160964 -1.34283839 -0.06266865 ... 0.36252649 -0.68519014
2.90917487]]
# Auswahl des Models
# solver = 'lbfgs' für kleine Datenmengen, solver = 'adam' für große Datenmengen, eher ab 10000
# hidden_layer: Anzahl der Neuronen pro verdeckte Schicht und Anzahl der verdeckten Schichten
model = MLPClassifier(solver='lbfgs', hidden_layer_sizes=[5, 5], max_iter=10000, random_state=42)
# Training
model.fit(X_train_skaliert, y_train)
# Validierung
score_train = model.score(X_train_skaliert, y_train)
score_test = model.score(X_test_skaliert, y_test)
print(f'Score für Trainingsdaten: {score_train:.2f}')
print(f'Score für Testdaten: {score_test:.2f}')
Score für Trainingsdaten: 0.81
Score für Testdaten: 0.70
Der Score der Trainingsdaten hat sich leicht verbessert, der Score für die Testdaten ist dafür leicht gesunken. Die Skalierung der Daten hat also einen Einfluss auf die Performance der ML-Modelle.
Zusammenfassung und Ausblick#
Daten sollten immer skaliert werden, sofern nicht Decision Trees oder Random Forests betrachtet werden. Es gibt zwei Möglichkeiten, Daten zu skalieren: Normierung oder Standardisierung. Letzteres wird häufiger verwendet, hängt aber natürlich von der Art der Daten ab.